package com.example.diancai.websocket.control;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.example.diancai.consts.RedisKeyConst;
import com.example.diancai.dto.CurrentPlayer;
import com.example.diancai.util.ContextUtil;
import com.example.diancai.util.RedisUtil;
import com.example.diancai.websocket.config.GetHttpSessionConfig;
import com.example.diancai.websocket.dto.ClientInfoDTO;
import com.example.diancai.websocket.dto.SocketMessage;
import com.example.diancai.websocket.logic.GameLogic;
import jakarta.annotation.PostConstruct;
import jakarta.websocket.*;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.CrossOrigin;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 该类负责监听客户端的连接、断开连接、接收消息、发送消息等操作。
 *
 * @author qf
 * @since 2024/08/29 19:50
 */
@Slf4j
@Component
@CrossOrigin(origins = "*")
@ServerEndpoint(value = "/webSocket/{token}", configurator = GetHttpSessionConfig.class)
public class WebSocketControl {
    //key：客户端连接唯一标识(token)
    //value：ClientInfoEntity
    public static final ConcurrentHashMap<String, ClientInfoDTO> userSocketMap = new ConcurrentHashMap<String, ClientInfoDTO>();


    private static final Duration EXIST_TIME_HOUR = Duration.ofMinutes(3);
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private GameLogic gameLogic;

    /**
     * 连接建立成功调用的方法
     *
     * @param session 第一个参数必须是session
     * @param sec
     * @param token   代表客户端的唯一标识
     */
    @OnOpen
    public void onOpen(Session session, EndpointConfig sec, @PathParam("token") String token) {
        log.info("open 当前:" + userSocketMap);
        if (redisUtil == null) {
            redisUtil = ContextUtil.getBean(RedisUtil.class);
        }
        if (gameLogic == null) {
            gameLogic = ContextUtil.getBean(GameLogic.class);
        }

        if (userSocketMap.containsKey(token)) {
            // 重连后重新建立连接
            ClientInfoDTO clientInfoDTO = userSocketMap.get(token);
            clientInfoDTO.setSession(session);
            clientInfoDTO.setOnline(true);
            clientInfoDTO.setExistTime(LocalDateTime.now().plus(EXIST_TIME_HOUR));

            this.reconnect(clientInfoDTO);
            return;
        }

        //把成功建立连接的会话在实体类中保存
        ClientInfoDTO entity = new ClientInfoDTO();
        entity.setToken(token);
        entity.setSession(session);
        entity.setOnline(true);

        CurrentPlayer activeUser = (CurrentPlayer) redisUtil.getObj(RedisKeyConst.AUTH + token, CurrentPlayer.class);
        if (activeUser == null) {
            log.error("没有找到当前用户:{}", token);
            //throw new RuntimeException("没有找到当前用户");
        }
        entity.setCurrentPlayer(activeUser);
        //默认连接6个小时
        entity.setExistTime(LocalDateTime.now().plus(EXIST_TIME_HOUR));
        userSocketMap.put(token, entity);
        //之所以获取http session 是为了获取获取httpsession中的数据 (用户名 /账号/信息)
        log.info("WebSocket 连接建立成功: " + token);
    }

    /**
     * 当断开连接时调用该方法
     *
     * @param session
     */
    @OnClose
    public void onClose(Session session, @PathParam("token") String token) {
        // 找到关闭会话对应的用户 ID 并从 uavWebSocketInfoMap 中移除
        if (ObjectUtil.isNotEmpty(token) && userSocketMap.containsKey(token)) {
            ClientInfoDTO clientInfoDTO = userSocketMap.get(token);
            offline(token, clientInfoDTO);
            log.info("WebSocket 连接关闭成功: " + token);
        }
    }

    /**
     * 接受消息
     * 这是接收和处理来自用户的消息的地方。我们需要在这里处理消息逻辑，可能包括广播消息给所有连接的用户。
     */
    @OnMessage
    public void onMessage(Session session, @PathParam("token") String token, String message) throws IOException {
        log.info("接收到消息：" + message);
        try {
            ClientInfoDTO entity = userSocketMap.get(token);
            //如果是心跳包
            if ("ping".equals(message)) {
                //只要接受到客户端的消息就进行续命(时间)
                entity.setExistTime(LocalDateTime.now().plus(EXIST_TIME_HOUR));
                if (entity.getSession().isOpen()) {
                    entity.getSession().getBasicRemote().sendText("read");
                }
                return;
            }
            //业务逻辑
            SocketMessage socketMessage = JSON.parseObject(message, SocketMessage.class);

            // 匹配
            if (socketMessage.getEvent().equals("match")) {
                gameLogic.match(entity, socketMessage.getData().toString(), false);
                return;
            }
            if (socketMessage.getEvent().equals("cancelMatch")) {
                // 取消匹配
                gameLogic.cancelMatch(entity);
                return;
            }
            // 匹配
            else if (socketMessage.getEvent().equals("match2048")) {
                gameLogic.match2048(entity);
                return;
            }
            // 获取房间信息
            else if (socketMessage.getEvent().equals("getRoomInfo")) {
                gameLogic.getRoomInfo(entity);
                return;
            } else if (socketMessage.getEvent().equals("updateGameInfo")) {
                gameLogic.updateGameInfo(entity, message);
                return;
            } else if (socketMessage.getEvent().equals("failure")) {
                gameLogic.failure(entity, message);
                return;
            } else if (socketMessage.getEvent().equals("addProp")) {
                gameLogic.addProp(entity, message);
                return;
            } else if (socketMessage.getEvent().equals("useProp")) {
                gameLogic.useProp(entity, message);
                return;
            } else if (socketMessage.getEvent().equals("usePropSuccess")) {
                gameLogic.usePropSuccess(entity, message);
                return;
            } else if (socketMessage.getEvent().equals("createRoom")) {
                gameLogic.createRoom(entity, message);
                return;
            } else if (socketMessage.getEvent().equals("joinRoom")) {
                gameLogic.joinRoom(entity, message);
                return;
            } else if (socketMessage.getEvent().equals("exitRoom")) {
                gameLogic.exitRoom(entity, message);
                return;
            } else if (socketMessage.getEvent().equals("updatePlayerStatus")) {
                gameLogic.updatePlayerStatus(entity, message);
                return;
            } else if (socketMessage.getEvent().equals("roomStartGame")) {
                gameLogic.roomStartGame(entity, message);
                return;
            }


            //只要接受到客户端的消息就进行续命(时间)
            entity.setExistTime(LocalDateTime.now().plus(EXIST_TIME_HOUR));
        } catch (Exception e) {
            log.info("消息处理异常", e);
        }


    }

    /**
     * 处理WebSocket中发生的任何异常。可以记录这些错误或尝试恢复。
     */
    @OnError
    public void onError(Throwable error) {
        log.error("报错信息：" + error.getMessage());
        error.printStackTrace();

    }



    /**
     * 发生消息定时器
     */
    @Scheduled(cron = "30 * *  * * ? ")
    public void refreshDateSize() {

        log.info("userSocketMap:{}",userSocketMap.size());
        log.info("roofSocketMap:{}",gameLogic.roofSocketMap.size());
        log.info("match2048Linked:{}",gameLogic.match2048Linked.size());
        log.info("matchxiaoxiaoleLinked:{}",gameLogic.matchxiaoxiaoleLinked.size());
    }
    /**
     * 发生消息定时器
     */
    @PostConstruct
    @Scheduled(cron = "0/1 * *  * * ? ")
    public void refreshDate() {
        //开启定时任务，1秒一次向前台发送当前时间
        //当没有客户端连接时阻塞等待

        if (!userSocketMap.isEmpty()) {
            //超过存活时间进行删除
            Iterator<Map.Entry<String, ClientInfoDTO>> iterator = userSocketMap.entrySet().iterator();
            while (iterator.hasNext()) {
                Map.Entry<String, ClientInfoDTO> entry = iterator.next();
                if (entry.getValue().getExistTime().compareTo(LocalDateTime.now()) <= 0) {
                    log.info("WebSocket " + entry.getKey() + " 已到存活时间，自动断开连接");
                    try {
                        entry.getValue().getSession().close();
                    } catch (IOException e) {
                        log.error("WebSocket 连接关闭失败: " + entry.getKey() + " - " + e.getMessage());
                    }

                    // 存在房间则退出
                    if (StrUtil.isNotEmpty(entry.getValue().getRoofNo())) {
                        exit(entry.getKey(), entry.getValue());
                    }


                    //过期则进行移除
                    iterator.remove();
                }
            }
            //sendMessage(FORMAT.format(new Date()));
        }

        // 房间时间到了，判断输赢
        gameLogic.findIsGameOver();
        gameLogic.matchLogic();

    }

    /**
     * 群发信息的方法
     *
     * @param message 消息
     */
    public void sendMessage(String message) {
        System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())
                + "发送全体消息:" + message);
        //循环客户端map发送消息
        userSocketMap.values().forEach(item -> {
            //向每个用户发送文本信息。这里getAsyncRemote()解释一下，向用户发送文本信息有两种方式，
            // 一种是getBasicRemote，一种是getAsyncRemote
            //区别：getAsyncRemote是异步的，不会阻塞，而getBasicRemote是同步的，会阻塞，由于同步特性，第二行的消息必须等待第一行的发送完成才能进行。
            // 而第一行的剩余部分消息要等第二行发送完才能继续发送，所以在第二行会抛出IllegalStateException异常。所以如果要使用getBasicRemote()同步发送消息
            // 则避免尽量一次发送全部消息，使用部分消息来发送，可以看到下面sendMessageToTarget方法内就用的getBasicRemote，因为这个方法是根据用户id来私发的，所以不是全部一起发送。
            item.getSession().getAsyncRemote().sendText(message);
        });
    }

    /**
     * 离线处理
     */
    public void offline(String token, ClientInfoDTO clientInfoDTO) {
        gameLogic.offline(clientInfoDTO);
    }

    /**
     * 已退出
     */
    public void exit(String token, ClientInfoDTO clientInfoDTO) {
        gameLogic.exit(clientInfoDTO);
    }

    public void reconnect(ClientInfoDTO clientInfoDTO) {
        gameLogic.reconnect(clientInfoDTO);
    }

    /**
     * 更新用户信息
     *
     * @param token
     * @param currPlayer
     */
    public void updatePlayerInfo(String token, CurrentPlayer currPlayer) {
        if (userSocketMap.containsKey(token)) {
            ClientInfoDTO clientInfoDTO = userSocketMap.get(token);
            clientInfoDTO.setCurrentPlayer(currPlayer);
        }
    }
}
